/*
    This file is part of Emu.

    Emu is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Emu is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with Emu.  If not, see <http://www.gnu.org/licenses/>. */
#include <QApplication>
#include <QHBoxLayout>
#include <QPushButton>
#include <QCheckBox>
#include <QStyle>

#include "esearchbar.h"

ESearchBar::ESearchBar(QTextEdit *recvWindow)
{
    _recvWindow = recvWindow;

    QIcon closeIcon = QApplication::style()->standardIcon(QStyle::SP_TitleBarCloseButton);
    QPushButton *searchCloseButton = new QPushButton(closeIcon, "");
    searchCloseButton->setToolTip(tr("Close Search Bar"));
    searchCloseButton->setStyleSheet("padding: 1px;");
    searchCloseButton->setFocusPolicy(Qt::NoFocus);

    _searchWindow = new QLineEdit;
    QPushButton *searchPrevButton = new QPushButton(tr("Find Previous"));
    searchPrevButton->setFocusPolicy(Qt::NoFocus);
    QPushButton *searchNextButton = new QPushButton(tr("Find Next"));
    searchNextButton->setFocusPolicy(Qt::NoFocus);
    _caseSensitiveCheck = new QCheckBox(tr("Match Case"));
    _caseSensitiveCheck->setFocusPolicy(Qt::NoFocus);

    QHBoxLayout *searchBarLayout = new QHBoxLayout;
    searchBarLayout->addWidget(searchCloseButton);
    searchBarLayout->addWidget(_searchWindow);
    searchBarLayout->addWidget(searchPrevButton);
    searchBarLayout->addWidget(searchNextButton);
    searchBarLayout->addWidget(_caseSensitiveCheck);
    /* This keeps the search window from growing as much as possible and pushing
       the other widgets to the right. */
    searchBarLayout->addStretch(10);
    /* Having extra unused top and bottom area would be nasty. So keep that from
       happening by setting the top and bottom margins to 0 for both layouts.
       But keep the left and right, because those are okay. */
    QMargins searchMargins = searchBarLayout->contentsMargins();
    searchBarLayout->setContentsMargins(searchMargins.left(), 0,
            searchMargins.right(), 0);
    this->setLayout(searchBarLayout);

    _lastSearchText = QString("");
    /* This is used so that the search window's palette doesn't have to be
       obtained and queried about the color color for each text change. */
    _lastSearchWasValid = true;
    _searchWasFoundBackColor = _searchWindow->palette().color(QPalette::Base);
    _searchWasFoundForeColor = _searchWindow->palette().color(QPalette::Text);
    _searchNotFoundBackColor = Qt::red;
    _searchNotFoundForeColor = Qt::white;
    _canCutCopy = false;
    _canUndo = false;
    _canRedo = false;
    _highlightFormat = QTextCharFormat();
    _highlightFormat.setForeground(QColor(Qt::black));
    _highlightFormat.setBackground(QColor(Qt::yellow));

    connect(searchCloseButton, SIGNAL(clicked()), this, SLOT(hide()));
    connect(_searchWindow, SIGNAL(textChanged(QString)), this,
            SLOT(catchSearchTextChanged(QString)));
    connect(searchPrevButton, SIGNAL(clicked()), this,
            SLOT(catchSearchPrevious()));
    connect(searchNextButton, SIGNAL(clicked()), this,
            SLOT(catchSearchNext()));
    connect(_searchWindow, SIGNAL(selectionChanged()), this,
            SLOT(catchSearchSelectionChanged()));
}

void ESearchBar::catchSearchSelectionChanged()
{
    if (_searchWindow->selectedText().isEmpty() != _canCutCopy) {
        if (_current)
            emit searchWindowCutCopyAvailable(
                    _searchWindow->selectedText().isEmpty() == false);
        _canCutCopy = !_canCutCopy;
    }
}

void ESearchBar::catchSearchNext()
{
    doSearch(true);
}

void ESearchBar::catchSearchPrevious()
{
    doSearch(false);
}

void ESearchBar::doSearch(bool forward)
{
    QTextDocument::FindFlags flags = 0;
    if (forward == false)
        flags |= QTextDocument::FindBackward;
    if (_caseSensitiveCheck->isChecked())
        flags |= QTextDocument::FindCaseSensitively;

    QTextCursor docCursor = _recvWindow->textCursor();
    QString searchText = _searchWindow->text();
    QTextCursor searchCursor = _recvWindow->document()->find(searchText,
            docCursor, flags);

    if (searchCursor.isNull()) {
        /* If searching forward, try again from the top of the document. Do the
           opposite if searching backwards. */
        QTextCursor::MoveOperation mop;
        if (forward)
            mop = QTextCursor::Start;
        else
            mop = QTextCursor::End;

        /* Do this so searchCursor knows that it works on _recvWindow.
           Otherwise, the search will always fail. */
        searchCursor = docCursor;
        searchCursor.movePosition(mop, QTextCursor::MoveAnchor);
        searchCursor = _recvWindow->document()->find(searchText, searchCursor,
                flags);
    }

    if (searchCursor.isNull() == false)
        _recvWindow->setTextCursor(searchCursor);

    bool thisSearchValid = (searchCursor.hasSelection() || searchText.isEmpty());

    if (thisSearchValid != _lastSearchWasValid) {
        updateSearchPalette(thisSearchValid);
        _lastSearchWasValid = thisSearchValid;
    }
}

void ESearchBar::catchSearchTextChanged(QString text)
{
    QTextCursor searchCursor;
    QTextCursor resultCursor;

    /* Qt won't call this if there's an undo/redo action and the text gets
       highlighted because of that. Ensure this is triggered, or cut/copy will
       not be up-to-date. */
    catchSearchSelectionChanged();

    if (_searchWindow->isUndoAvailable() != _canUndo) {
        if (_current)
            emit searchWindowUndoAvailable(_searchWindow->isUndoAvailable());
        _canUndo = !_canUndo;
    }
    if (_searchWindow->isRedoAvailable() != _canRedo) {
        if (_current)
            emit searchWindowRedoAvailable(_searchWindow->isRedoAvailable());
        _canRedo = !_canRedo;
    }

    QTextDocument::FindFlags flags = 0;
    if (_caseSensitiveCheck->isChecked())
        flags |= QTextDocument::FindCaseSensitively;

    /* This highlights everything that matches the current selection. If the
       text is empty, there will be no matches which will clear everything. */
    {
        QTextCursor highlightCursor = _recvWindow->textCursor();
        highlightCursor.movePosition(QTextCursor::Start);
        QList<QTextEdit::ExtraSelection> extras;
        while (1) {
            highlightCursor = _recvWindow->document()->find(text,
                    highlightCursor, flags);
            if (highlightCursor.isNull())
                break;

            QTextEdit::ExtraSelection selection;
            selection.cursor = highlightCursor;
            selection.format = _highlightFormat;
            extras.append(selection);
        }
        _recvWindow->setExtraSelections(extras);
    }


    if (text.isEmpty()) {
        _lastSearchText = QString("");
        resultCursor = _recvWindow->textCursor();
        /* Nothing to search for, so make the selection go away. */
        resultCursor.setPosition(resultCursor.position());
        _recvWindow->setTextCursor(resultCursor);
    }
    else if (_lastSearchText.isEmpty()) {
        _lastSearchText = text;

        searchCursor = _recvWindow->cursorForPosition(QPoint(1, 1));
        resultCursor = _recvWindow->document()->find(text, searchCursor, flags);

        if (resultCursor.isNull() == false)
            _recvWindow->setTextCursor(resultCursor);
    }
    else {
        searchCursor = _recvWindow->textCursor();
        /* Adjust based on the current cursor. */
        QString selectionText;

        /* If there is a selection, pick that. Otherwise, pick the selection
           from the last valid search. If a user enters something that does not
           match, they simply delete the non-matching part and the cursor will
           highlight what was used previously. If the new text does not match,
           then it will use an area near the last valid search. */
        if (searchCursor.hasSelection())
            selectionText = searchCursor.selectedText();
        else
            selectionText = _lastSearchText;

        int selectionLen = selectionText.length();

        /* The cursor's position is between characters at the end of the
           selection. Move the cursor to the left to account for whatever text
           is the largest. This will cause the search to highlight near the
           current/last selection first, if possible. */
        int searchPos = searchCursor.position();
        if (text.length() > selectionLen)
            searchPos -= text.length();
        else
            searchPos -= selectionLen;

        if (searchPos < 0)
            searchPos = 0;

        resultCursor = _recvWindow->document()->find(text, searchPos, flags);

        /* Retry at the top of the document, to be consistent with the next
           button. */
        if (resultCursor.isNull())
            resultCursor = _recvWindow->document()->find(text, 0, flags);

        if (resultCursor.isNull() == false) {
            _lastSearchText = text;
            _recvWindow->setTextCursor(resultCursor);
        }
        else {
            resultCursor = searchCursor;
            resultCursor.setPosition(searchPos);
            _recvWindow->setTextCursor(resultCursor);
        }
    }

    bool thisSearchValid = (resultCursor.hasSelection() || text.isEmpty());

    if (thisSearchValid != _lastSearchWasValid) {
        updateSearchPalette(thisSearchValid);
        _lastSearchWasValid = thisSearchValid;
    }
}


void ESearchBar::updateSearchPalette(bool searchFound)
{
    QColor backColor, foreColor;
    QPalette palette;

    if (searchFound) {
        backColor = _searchWasFoundBackColor;
        foreColor = _searchWasFoundForeColor;
    }
    else {
        backColor = _searchNotFoundBackColor;
        foreColor = _searchNotFoundForeColor;
    }

    palette.setColor(QPalette::Base, backColor);
    palette.setColor(QPalette::Text, foreColor);
    _searchWindow->setPalette(palette);
}